﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Futaba.SBus.Extensions;
using System.Text;
using System.ComponentModel;
using System.Reflection;

namespace Futaba.SBus.Telemetry.Configuration
{
	/// <summary>
	/// Handles loading and saving of <see cref="Sensor">Sensors</see>
	/// </summary>
	public class SensorFactory
	{
		private Sensor[] _knownSensors;

		#region Singleton

		private static SensorFactory _singletonSensorFactory;
		private static readonly object SensorFactorySingletonLock = new object();

		/// <summary>
		/// A singleton of this class
		/// </summary>
		public static SensorFactory DefaultInstance
		{
			get
			{
				if (_singletonSensorFactory == null)
				{
					// ReSharper disable PossibleMultipleWriteAccessInDoubleCheckLocking
					lock (SensorFactorySingletonLock)
					{
						if (_singletonSensorFactory == null)
							_singletonSensorFactory = new SensorFactory();
					}
					// ReSharper restore PossibleMultipleWriteAccessInDoubleCheckLocking
				}
				return _singletonSensorFactory;
			}
		}

		#endregion

		/// <summary>
		/// Returns the list of known sensors (at compile-time) from an internal ressource
		/// </summary>
		/// <returns></returns>
		public Sensor[] GetKnownSensors()
		{
			if (_knownSensors == null)
			{
				var type = typeof (SensorFactory);
				using (var stream = type.Assembly.GetManifestResourceStream(type.Namespace + ".SensorConfiguration.xml"))
				{
					_knownSensors = Load(stream);
				}
			}
			return _knownSensors;
		}

		/// <summary>
		/// Loads and returns the list of <see cref="Sensor">sensors</see> from the provided configuration file
		/// </summary>
		/// <param name="xmlConfiguration">the file to load the sensors from</param>
		/// <returns></returns>
		/// <seealso cref="Save"/>
		public Sensor[] Load(Stream xmlConfiguration)
		{
			var sensors = xmlConfiguration.Load<Sensor[]>(null);
			Validate(sensors);
			return sensors;
		}

		/// <summary>
		/// Replaces the default list returned by <see cref="GetKnownSensors"/>
		/// for this instance
		/// </summary>
		public void SetKnownSensors(Sensor[] sensors)
		{
			_knownSensors = sensors;
		}

		/// <summary>
		/// Validates the given sensors for correctness
		/// </summary>
		/// <exception cref="ConfigurationException">Thrown, if any errors are detected</exception>
		public virtual void Validate(IEnumerable<Sensor> sensors)
		{
			var errors = new List<string>();
			Validate(sensors, errors);

			if (errors.Any())
			{
				var msg = string.Join("\n", errors.ToArray());
				throw new ConfigurationException(msg);
			}
		}

		/// <summary>
		/// Adds messages to the <paramref name="errors"/> collection for all configuration errors detected
		/// </summary>
		protected virtual void Validate(IEnumerable<Sensor> sensors, List<string> errors)
		{
			var types = sensors.GroupBy(s => s.SensorType).Where(g => g.Count() > 1).Select(g => g.Key.ToString()).ToArray();
			if (types.Any())
			{
				errors.Add(string.Format("SensorTypes {0} are configured more than once!", string.Join(", ", types)));
			}

			foreach (var s in sensors)
			{
				var coordParams = s.Parameters.Where(p => (p.ParsingFlags & SensorValueParsingFlags.Coordinates) != 0);
				foreach (var p in coordParams)
				{
					if (p.ConsumesSlots != 2)
						errors.Add(string.Format("Sensor {0}, Parameter {1}: ParsingFlag Coordinates requires ConsumesSlots to be 2",
							s.SensorType, p.Position));
				}
			}
		}

		/// <summary>
		/// Saves the supplied <see cref="Sensor">sensors</see> to the provided stream as XML
		/// </summary>
		/// <param name="sensors">the sensors to save</param>
		/// <param name="outFile">the stream to write the XML to</param>
		/// <param name="encoding">the optional encoding to use for XML serialization.
		/// If not supplied, <see cref="Encoding.Default"/> is used instead</param>
		public void Save(Sensor[] sensors, Stream outFile, Encoding encoding = null)
		{
			sensors.ToXml(outFile, encoding ?? Encoding.Default, null);
		}

		private static Dictionary<string, SensorType> _sensorTypeDescriptions;

		/// <summary>
		/// Checks wether the given <paramref name="sensorName"/> is a well-known
		/// <see cref="SensorType">sensor</see>
		/// </summary>
		/// <remarks>
		/// The sensor-name of each <see cref="SensorType">sensor</see> is retrieved
		/// using the DescriptionAttribute of each enum-element. Futaba defines the 
		/// names for each sensor, which you can retrieve by generating a CSV file from
		/// a FLI file
		/// </remarks>
		/// <param name="sensorName">the sensor's name to check</param>
		/// <returns>True, if it is a known sensor, else false</returns>
		public bool IsKnownSensor(string sensorName)
		{
			if (_sensorTypeDescriptions == null)
				PopulateSensorTypeDescriptions();

			// ReSharper disable once PossibleNullReferenceException
			return _sensorTypeDescriptions.Keys.Any(k => k.Equals(sensorName, StringComparison.InvariantCultureIgnoreCase));
		}

		/// <summary>
		/// Checks wether the given <paramref name="sensorName"/> is a well-known
		/// <see cref="SensorType">sensor</see>
		/// </summary>
		/// <param name="sensorName">the sensor's name to check</param>
		/// <returns>The SensorType of the given <paramref name="sensorName"/>, or SensorType.Unknown</returns>
		public SensorType GetKnownSensorType(string sensorName)
		{
			if (_sensorTypeDescriptions == null)
				PopulateSensorTypeDescriptions();

			// ReSharper disable once PossibleNullReferenceException
			var key =
				_sensorTypeDescriptions.Keys.FirstOrDefault(k => k.Equals(sensorName, StringComparison.InvariantCultureIgnoreCase));
			return key == null ? SensorType.Unknown : _sensorTypeDescriptions[key];
		}

		private void PopulateSensorTypeDescriptions()
		{
			var types = Enum.GetValues(typeof (SensorType));
			var output = new Dictionary<string, SensorType>();
			foreach (var t in types)
			{
				output.Add(t.ToString(), (SensorType) t);
				FieldInfo fi = t.GetType().GetField(t.ToString());
				var descAttribute =
					fi.GetCustomAttributes(typeof (DescriptionAttribute), true).FirstOrDefault() as DescriptionAttribute;
				if (descAttribute == null)
					continue;
				var descs = descAttribute.Description.Split(';');
				foreach (var d in descs)
				{
					if (!output.ContainsKey(d))
						output.Add(d, (SensorType) t);
				}
			}
			_sensorTypeDescriptions = output;
		}
	}
}